Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 15x 15x 15x 15x 15x 15x 1x 2x 2x 2x 2x 1x 1x 1x 2x 2x 1x 15x 15x 15x 15x 15x 15x 15x 7x 7x 7x 15x 2x 2x 5x 15x 15x 1x 1x 4x 4x 4x 15x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 1x 2x 15x 15x 1x 1x 1x 15x | import { authorizeKidSongsRequest } from '@/lib/auth/integrationToken'
import { readAndHashSong } from '@/lib/kid-songs/audio'
import {
audioUrlFor,
isValidId,
toEligibleSongCandidate,
toIso,
toNullableIso,
} from '@/lib/kid-songs/eligibility'
import { getLatestSongCandidates, getSongCandidateById } from '@/lib/kid-songs/queries'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
const headers = { 'Cache-Control': 'no-store', Vary: 'Authorization' }
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { ...headers, 'Content-Type': 'application/json' },
})
}
async function isFullyEligible(playerId: string, songId: string): Promise<boolean> {
const row = await getSongCandidateById(playerId, songId)
const candidate = row ? toEligibleSongCandidate(row) : null
if (!candidate) return false
try {
await readAndHashSong(candidate.id)
return true
} catch (error) {
console.error(`[kid-songs] Unable to hash known song ${candidate.id}:`, error)
return false
}
}
export async function GET(
request: Request,
context: { params: Promise<{ playerId: string }> }
): Promise<Response> {
try {
const auth = authorizeKidSongsRequest(request)
if (!auth.configured || !auth.tokenAccepted) return json({ error: 'Unauthorized' }, 401)
const { playerId } = await context.params
const knownSongId = new URL(request.url).searchParams.get('knownSongId')
if (!isValidId(playerId) || (knownSongId !== null && !isValidId(knownSongId))) {
return json({ error: 'Invalid parameters' }, 400)
}
const emptyKnown = knownSongId === null ? null : { id: knownSongId, eligible: false }
if (!auth.allowedPlayerIds.has(playerId)) {
return json({ song: null, knownSong: emptyKnown })
}
let song = null
const candidates = await getLatestSongCandidates(playerId)
for (const row of candidates) {
const candidate = toEligibleSongCandidate(row)
if (!candidate) continue
try {
const audio = await readAndHashSong(candidate.id)
song = {
id: candidate.id,
playerId: candidate.playerId,
title: candidate.title,
durationSeconds: candidate.durationSeconds,
createdAt: toIso(candidate.createdAt),
completedAt: toNullableIso(candidate.completedAt),
audioVersion: audio.sha256,
audioSha256: audio.sha256,
audioUrl: audioUrlFor(candidate.playerId, candidate.id, audio.sha256),
}
break
} catch (error) {
console.error(`[kid-songs] Skipping unreadable song ${candidate.id}:`, error)
}
}
const knownSong =
knownSongId === null
? null
: { id: knownSongId, eligible: await isFullyEligible(playerId, knownSongId) }
return json({ song, knownSong })
} catch (error) {
console.error('[kid-songs] Latest route failed:', error)
return json({ error: 'Internal error' }, 500)
}
}
|